// Copyright 2011 George Cook
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#import "InjectiveContainer.h"
#import "InjectiveObjectDescription.h"
#import "InjectivePropertyDescription.h"

#import "InjectiveFactory.h"

#import "InjectiveCache.h"
#import "InjectiveSingletonCache.h"

#import <objc/runtime.h>

@interface InjectiveContainer ()

- (NSString *) getTypeNameFor:(id) type;
- (void) raiseExceptionWithMessage: (NSString *) message isAdding:(BOOL) isAdding;
- (id) getInstance:(InjectiveObjectDescription *) objectDescription;
- (id) instantiateObject:(InjectiveObjectDescription *) objectDescription;
- (BOOL) isTypeOf:(id) type objectDescription:(InjectiveObjectDescription *) objectDescription;
- (InjectiveObjectDescription *) getObjectDescriptionForType:(id) clazzType;
- (id)getObjectByAssociatedType:(id) type;
- (BOOL) typeIsProtocol:(id) object;
- (void) addObjectByID:(InjectiveObjectDescription *)objectDescription overrideIfIdsAreEqual:(BOOL) isOverriding;
- (void) addObjectByType:(InjectiveObjectDescription *)objectDescription overrideIfIdsAreEqual:(BOOL) isOverriding;
@end

@implementation InjectiveContainer

@synthesize instanceFactory;
@synthesize instanceCache;

- (void)dealloc
{
	[objectDescriptions release], objectDescriptions = nil;
	[objectDescriptionsByID release], objectDescriptionsByID = nil;
	[objectDescriptionsByType release], objectDescriptionsByType = nil;
	[instanceFactory release], instanceFactory = nil;
	[instanceCache release], instanceCache = nil;
	[super dealloc];
}

- (id)init
{
	if (self = [super init]) {
		[self clearContext];
	}
	return self;
}

#pragma mark -
#pragma mark - Public API

- (NSArray *)objectDescriptionsInContext
{
	return [objectDescriptions allValues];
}

- (void) wireInstance:(id)instance
{
	InjectiveObjectDescription *objectDescription = [objectDescriptionsByType objectForKey:[instance class]];
	NSArray *propertyDescriptions = [objectDescription propertyDescriptions];
	[instanceFactory autoWireDependenciesWith:instance withPropertyDescriptions:propertyDescriptions];
}

- (void) addObject:(InjectiveObjectDescription *) objectDescription
{
	[self addObject:objectDescription overrideIfIdsAreEqual:NO];
}

- (void) addObject:(InjectiveObjectDescription *)objectDescription overrideIfIdsAreEqual:(BOOL) isOverriding
{
	[objectDescriptions setObject:objectDescription forKey:objectDescription.key];
	if (objectDescription.ID != nil) {
		[self addObjectByID:objectDescription overrideIfIdsAreEqual:isOverriding];
	}
	else{
		[self addObjectByType:objectDescription overrideIfIdsAreEqual:isOverriding];
	}
}

- (void) addObjectByID:(InjectiveObjectDescription *)objectDescription overrideIfIdsAreEqual:(BOOL) isOverriding
{
	InjectiveObjectDescription *existingDescription = [objectDescriptionsByID objectForKey:objectDescription.ID];
	if (nil != existingDescription) {
		if (!isOverriding || hasRetrievedInstances) {
			NSString *reason = !isOverriding ? @" %@ id is already in use for type %@" : @"For consistency sake you cannot do overrides after retrieving an instnace, clear the context first : id %@, type";
			[self raiseExceptionWithMessage:[NSString stringWithFormat:reason, objectDescription.ID, objectDescription.type]
								   isAdding:YES];
		}
		else {
			NSLog(@"\n=======\nWarning - Overriding object description for object with ID: %@ and type: %@  for object with type %@ \n - ARE YOU SURE YOU ARE INTENDING TO DO THIS - (e.g. is this unit test/simulator/PROD CODE???)", objectDescription.ID, existingDescription.type, objectDescription.type);
		}
		[objectDescriptions removeObjectForKey:existingDescription.key];
	}
	[objectDescriptionsByID setObject:objectDescription forKey:objectDescription.ID];
}

- (void) addObjectByType:(InjectiveObjectDescription *)objectDescription overrideIfIdsAreEqual:(BOOL) isOverriding
{
	if (nil != [objectDescriptionsByType objectForKey:objectDescription.type]) {
		NSString *reason = !isOverriding ? @"The type %@ is already in use" : @"You can only overide descriptions of type id. - you tried to override type %@";
		[self raiseExceptionWithMessage:[NSString stringWithFormat:reason, objectDescription.type]
							   isAdding:YES];
	}
	[objectDescriptionsByType setObject:objectDescription forKey:objectDescription.type];
}

- (id) getObjectByID:(NSString *)ID
{
	InjectiveObjectDescription *objectDescription = [objectDescriptionsByID objectForKey:ID];
	if ( nil == objectDescription) {
		[self raiseExceptionWithMessage:[NSString stringWithFormat:@"Couldn't find Object with ID %@",ID]
							   isAdding:NO];
	}
	return [self getInstance:objectDescription];
}

- (id) getObjectByPropertyDescription:(InjectivePropertyDescription *)propertyDescription
{
	if ( nil != propertyDescription.idRef) {
		return [self getObjectByID:propertyDescription.idRef];
	}
	else if ( nil != propertyDescription.type)
	{
		return [self getObject:propertyDescription.type];
	}
	return nil;
}

- (id) getObject:(id)clazz
{
	NSString *clazzType = [[self getTypeNameFor: clazz] retain];
	InjectiveObjectDescription *objectDescription = [self getObjectDescriptionForType:clazzType];
	if (nil == objectDescription) {
		[self raiseExceptionWithMessage:[NSString stringWithFormat:@"No ObjectDescription for type %@", clazzType]
							   isAdding:NO];
	}
	
	return [self getInstance:objectDescription];
}


- (InjectiveObjectDescription *) getObjectDescriptionForType:(id) clazzType
{
	InjectiveObjectDescription *objectDescription = [objectDescriptionsByType objectForKey:clazzType];
	if (nil == objectDescription) {
		objectDescription = [self getObjectByAssociatedType:clazzType];
	}
	return objectDescription;
}


- (id) getInstance:(InjectiveObjectDescription *) objectDescription
{
	id instance = [instanceCache getInstanceWithKey:objectDescription.key];
	if (nil == instance) {
		instance = [self instantiateObject:objectDescription];
	}
	hasRetrievedInstances = YES;
	return instance;
}

- (id) instantiateObject:(InjectiveObjectDescription *) objectDescription
{
	id instance = instance = [instanceFactory createInstanceFor:objectDescription];
	
	if (objectDescription.isSingleton) {
		[instanceCache storeInstance:instance withKey:objectDescription.key];
	}
	return instance;
}

- (void) clearContext   //Supports unit testing... Only call this if you know what you're doing!
{
	objectDescriptionsByType = [[NSMutableDictionary dictionary] retain];
	objectDescriptions = [[NSMutableDictionary dictionary] retain];
	objectDescriptionsByID = [[NSMutableDictionary dictionary] retain];
	instanceCache = [[InjectiveSingletonCache alloc] init];
	instanceFactory = [[InjectiveFactory alloc] init];
	hasRetrievedInstances = NO;
	[instanceFactory setContainer:self];
}

/**
 Assumes any instance added is a singleton. Creates object Description automatically, based on class
 */
- (void) addInstance:(id)instance
{
	[self addInstance:instance forType:nil];
}

- (void) addInstance:(id)instance forType:(id)type
{
	if (nil != instance) {
		if ( nil != instanceCache) {
			NSString *clazzType = [[self getTypeNameFor: type == nil ? [instance class] : type] retain];
			
			InjectiveObjectDescription *objectDescription = [self getObjectDescriptionForType:clazzType];
			if (nil == objectDescription) {
				objectDescription = [[InjectiveObjectDescription alloc]
									 initWithType:clazzType andID:nil];
				[objectDescriptions setObject:objectDescription forKey:objectDescription.key];
				[objectDescriptionsByType setObject:objectDescription forKey:objectDescription.type];
			}
			[clazzType release];
			if (nil != [instanceCache getInstanceWithKey:objectDescription.key]) {
				[self raiseExceptionWithMessage:@"There is already an instance in  place for this type" isAdding:YES];
			}
			[instanceCache storeInstance:instance withKey:objectDescription.key];
		}
		else {
			[self raiseExceptionWithMessage:@"you need an instance cache to add instances" isAdding:YES];
		}
		
	}
	else {
		[self raiseExceptionWithMessage:@"You cannot add a nil instance" isAdding:YES];
	}
}


#pragma mark -
#pragma mark Private methods
- (void) raiseExceptionWithMessage: (NSString *) message isAdding:(BOOL) isAdding
{
	NSString *name = isAdding ? @"INJECTIVE CONTAINER ERROR: Error adding Object Description" :
	@"INJECTIVE CONTAINER ERROR: Error retrieving Object";
	NSException *ex = [NSException exceptionWithName:name reason:message userInfo:nil];
	[ex raise];
}

- (id)getObjectByAssociatedType:(id) type
{
	//pseudo
	//first check lookup cache
	NSString *key = [objectDescriptionByClazzLookupCache objectForKey:type];
	if (nil == key) {
		BOOL isMatched = NO;
		for (key in [objectDescriptions allKeys]) {
			InjectiveObjectDescription *objectDescripion = [objectDescriptions objectForKey:key];
			if ([self isTypeOf:type objectDescription:objectDescripion]) {
				key = objectDescripion.key;
				NSString *typeName = [self getTypeNameFor:type];
				[objectDescriptionByClazzLookupCache setObject:key forKey:typeName];
				isMatched = YES;
				break;
			}
		}
		if (!isMatched) {
			return nil;
		}
	}
	return [objectDescriptions objectForKey:key];
}

- (BOOL) isTypeOf:(id) type objectDescription:(InjectiveObjectDescription *) objectDescription
{
	Class targetClass = NSClassFromString(objectDescription.type);
	Class typeClass = NSClassFromString(type);
	if ( nil != typeClass && ([typeClass isSubclassOfClass:targetClass] || [targetClass isSubclassOfClass:typeClass])) {
		return YES;
	}
	else {
		Protocol *typeProtocol = NSProtocolFromString(type);
		
		if (nil != typeProtocol && [targetClass conformsToProtocol:typeProtocol]) {
			return YES;
		}
	}
	return NO;
}


- (NSString *) getTypeNameFor:(id) type
{
	if ([self typeIsProtocol:type]) {
		return NSStringFromProtocol(type);
	}
	else if ([type isKindOfClass:[NSString class]])
	{
		return type;
	}
	return NSStringFromClass(type);
}

- (BOOL) typeIsProtocol:(id) object
{
	return [NSStringFromClass(object_getClass(object)) isEqual:@"Protocol"];
}

- (NSString *) description
{
	return [[super description] stringByAppendingFormat:@" currentObject context is: \n======%@ \nand cached Objects are \n%@\n=======",
			objectDescriptions, instanceCache];
}


@end
